package com.alecgorge.minecraft.jsonapi.packets.netty.router;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.QueryStringDecoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.code.regexp.NamedMatcher;
import com.google.code.regexp.NamedPattern;
/**
* This class allows you to do route requests based on the HTTP verb and the
* request URI, in a manner similar to <a
* href="http://www.sinatrarb.com/">Sinatra</a> or <a
* href="http://expressjs.com/">Express</a>.
* <p>
* RouteMatcher also lets you extract parameters from the request URI either a
* simple pattern or using regular expressions for more complex matches. Any
* parameters extracted will be added to the requests parameters which will be
* available to you in your request handler.
* <p>
* It's particularly useful when writing REST-ful web applications.
* <p>
* To use a simple pattern to extract parameters simply prefix the parameter
* name in the pattern with a ':' (colon).
* <p>
* Different handlers can be specified for each of the HTTP verbs, GET, POST,
* PUT, DELETE etc.
* <p>
* For more complex matches regular expressions can be used in the pattern. When
* regular expressions are used, the extracted parameters do not have a name, so
* they are put into the HTTP request with names of param0, param1, param2 etc.
* <p>
* Multiple matches can be specified for each HTTP verb. In the case there are
* more than one matching patterns for a particular request, the first matching
* one will be used.
* <p>
* Instances of this class are not thread-safe
* <p>
*
* @author <a href="http://tfox.org">Tim Fox</a>
* @author <a href="http://alecgorge.com">Alec Gorge</a>
*/
public class RouteMatcher extends SimpleChannelInboundHandler<FullHttpRequest> {
private final List<PatternBinding> getBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> putBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> postBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> deleteBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> optionsBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> headBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> traceBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> connectBindings = new ArrayList<PatternBinding>();
private final List<PatternBinding> patchBindings = new ArrayList<PatternBinding>();
private Handler<FullHttpResponse, RoutedHttpRequest> noMatchHandler;
private Handler<Void, RoutedHttpResponse> everyMatchHandler;
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
serveRequest(ctx, request);
}
List<PatternBinding> getBindingsForRequest(FullHttpRequest request) {
HttpMethod m = request.getMethod();
if (m.equals(HttpMethod.GET)) {
return getBindings;
}
else if (m.equals(HttpMethod.PUT)) {
return putBindings;
}
else if (m.equals(HttpMethod.POST)) {
return postBindings;
}
else if (m.equals(HttpMethod.DELETE)) {
return deleteBindings;
}
else if (m.equals(HttpMethod.OPTIONS)) {
return optionsBindings;
}
else if (m.equals(HttpMethod.HEAD)) {
return headBindings;
}
else if (m.equals(HttpMethod.TRACE)) {
return traceBindings;
}
else if (m.equals(HttpMethod.PATCH)) {
return patchBindings;
}
else if (m.equals(HttpMethod.CONNECT)) {
return connectBindings;
}
return null;
}
public boolean serveRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
// Handle a bad request.
if (!request.getDecoderResult().isSuccess()) {
sendHttpResponse(ctx, request, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST));
return false;
}
return route(ctx, request, getBindingsForRequest(request));
}
/**
* Specify a handler that will be called for a matching HTTP GET
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher get(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, getBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PUT
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher put(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, putBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP POST
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher post(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, postBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP DELETE
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher delete(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, deleteBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP OPTIONS
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher options(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, optionsBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP HEAD
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher head(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, headBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP TRACE
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher trace(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, traceBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP CONNECT
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher connect(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, connectBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PATCH
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher patch(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called for all HTTP methods
*
* @param pattern
* The simple pattern
* @param handler
* The handler to call
*/
public RouteMatcher all(String pattern, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addPattern(pattern, handler, getBindings);
addPattern(pattern, handler, putBindings);
addPattern(pattern, handler, postBindings);
addPattern(pattern, handler, deleteBindings);
addPattern(pattern, handler, optionsBindings);
addPattern(pattern, handler, headBindings);
addPattern(pattern, handler, traceBindings);
addPattern(pattern, handler, connectBindings);
addPattern(pattern, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP GET
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher getWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, getBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PUT
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher putWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, putBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP POST
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher postWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, postBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP DELETE
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher deleteWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, deleteBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP OPTIONS
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher optionsWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, optionsBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP HEAD
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher headWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, headBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP TRACE
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher traceWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, traceBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP CONNECT
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher connectWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, connectBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PATCH
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher patchWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called for all HTTP methods
*
* @param regex
* A regular expression
* @param handler
* The handler to call
*/
public RouteMatcher allWithRegEx(String regex, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
addRegEx(regex, handler, getBindings);
addRegEx(regex, handler, putBindings);
addRegEx(regex, handler, postBindings);
addRegEx(regex, handler, deleteBindings);
addRegEx(regex, handler, optionsBindings);
addRegEx(regex, handler, headBindings);
addRegEx(regex, handler, traceBindings);
addRegEx(regex, handler, connectBindings);
addRegEx(regex, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called when no other handlers match. If
* this handler is not specified default behaviour is to return a 404
*/
public RouteMatcher noMatch(Handler<FullHttpResponse, RoutedHttpRequest> handler) {
noMatchHandler = handler;
return this;
}
/**
* Specify a handler that will be called when any other handler matchs.
*/
public RouteMatcher everyMatch(Handler<Void, RoutedHttpResponse> handler) {
everyMatchHandler = handler;
return this;
}
private static void addPattern(String input, Handler<FullHttpResponse, RoutedHttpRequest> handler, List<PatternBinding> bindings) {
// We need to search for any :<token name> tokens in the String and
// replace them with named capture groups
Matcher m = Pattern.compile(":([A-Za-z][A-Za-z0-9_]*)").matcher(input);
StringBuffer sb = new StringBuffer();
Set<String> groups = new HashSet<String>();
while (m.find()) {
String group = m.group().substring(1);
if (groups.contains(group)) {
throw new IllegalArgumentException("Cannot use identifier " + group + " more than once in pattern string");
}
m.appendReplacement(sb, "(?<$1>[^\\/]+)");
groups.add(group);
}
m.appendTail(sb);
String regex = sb.toString();
PatternBinding binding = new PatternBinding(NamedPattern.compile(regex), groups, handler);
bindings.add(binding);
}
private static void addRegEx(String input, Handler<FullHttpResponse, RoutedHttpRequest> handler, List<PatternBinding> bindings) {
PatternBinding binding = new PatternBinding(NamedPattern.compile(input), null, handler);
bindings.add(binding);
}
public FullHttpResponse getResponse(ChannelHandlerContext ctx, FullHttpRequest request) {
// Handle a bad request.
FullHttpResponse resp = null;
if (!request.getDecoderResult().isSuccess()) {
resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);
sendHttpResponse(ctx, request, resp);
}
else {
resp = getResponseForRoute(ctx, request, getBindingsForRequest(request));
}
return resp;
}
FullHttpResponse getResponseForRoute(ChannelHandlerContext ctx, FullHttpRequest request, List<PatternBinding> bindings) {
RoutedHttpRequest rreq = new RoutedHttpRequest(ctx, request);
for (PatternBinding binding : bindings) {
QueryStringDecoder uri = new QueryStringDecoder(request.getUri());
NamedMatcher m = binding.pattern.matcher(uri.path());
if (m.matches()) {
Map<String, List<String>> params = new HashMap<String, List<String>>(m.groupCount());
if (binding.paramNames != null) {
// Named params
for (String param : binding.paramNames) {
List<String> l = new ArrayList<String>();
l.add(m.group(param));
params.put(param, l);
}
}
else {
// Un-named params
for (int i = 0; i < m.groupCount(); i++) {
List<String> l = new ArrayList<String>();
l.add(m.group(i + 1));
params.put("param" + i, l);
}
}
uri.parameters().putAll(params);
FullHttpResponse res = binding.handler.handle(rreq);
return res;
}
}
if (noMatchHandler != null) {
return noMatchHandler.handle(rreq);
}
return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND);
}
private boolean route(ChannelHandlerContext ctx, FullHttpRequest request, List<PatternBinding> bindings) {
FullHttpResponse res = getResponseForRoute(ctx, request, bindings);
sendHttpResponse(ctx, request, res);
if (everyMatchHandler != null) {
everyMatchHandler.handle(new RoutedHttpResponse(request, res));
}
return noMatchHandler != null;
}
void sendHttpResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse res) {
if (res == null) {
// no http response, probably upgrading to websocket or something
return;
}
// Send the response and close the connection if necessary.
ChannelFuture f = ctx.channel().writeAndFlush(res);
if (!HttpHeaders.isKeepAlive(req) || res.getStatus().code() != 200) {
f.addListener(ChannelFutureListener.CLOSE);
}
}
private static class PatternBinding {
final NamedPattern pattern;
final Handler<FullHttpResponse, RoutedHttpRequest> handler;
final Set<String> paramNames;
private PatternBinding(NamedPattern pattern, Set<String> paramNames, Handler<FullHttpResponse, RoutedHttpRequest> handler) {
this.pattern = pattern;
this.paramNames = paramNames;
this.handler = handler;
}
}
}